/*
 * @Description:
 * @Author: Cxy
 * @Date: 2024-08-29 15:41:37
 * @LastEditors: Cxy
 * @LastEditTime: 2024-09-12 21:48:27
 * @FilePath: \xy-admin\play\src\utils\watermark.ts
 */

interface Font {
  fontSize?: number
  fontFamily?: string
  fontWeight?: string
  color?: string
  textAlign?: CanvasTextAlign
}
export type Config = {
  rotate?: number
  text?: string[] | string
  image?: {
    src: string
    scale?: number
  }
  font?: Font
  gap?: [number, number]
  offset?: [number, number]
  targetElement: HTMLElement | string
  isDisplay?: boolean
}

const canvasStyle = {
  position: 'absolute',
  top: '0',
  left: '0',
  'pointer-events': 'none',
  'z-index': '2006'
}
class Watermark {
  canvas: HTMLCanvasElement
  ctx: CanvasRenderingContext2D
  config: Config
  content: string[] | HTMLImageElement = []
  resizeObserver: ResizeObserver | null = null
  changeObserver: MutationObserver | null = null
  constructor(config: Config) {
    this.config = config
    const canvas = document.createElement('canvas')
    this.canvas = canvas
    this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D
    Object.assign(canvas.style, canvasStyle)
  }
  async getTargetElementDom() {
    const { config } = this
    await nextTick()
    return typeof config.targetElement === 'string'
      ? document.querySelector(config.targetElement) || document.body
      : config.targetElement
  }
  async initObserverDom(config: Omit<Config, 'targetElement'>) {
    const targetElement = await this.getTargetElementDom()
    if (targetElement.contains(this.canvas)) return

    if (window.ResizeObserver) {
      this.resizeObserver = new ResizeObserver(() => this.drawCanvas())
      this.resizeObserver.observe(targetElement)
    } else {
      this.drawCanvas = this.drawCanvas.bind(this)
      window.addEventListener('resize', this.drawCanvas)
    }

    this.changeObserver = new MutationObserver(mutations => this.changeStyleDom(mutations))
    this.changeObserver.observe(targetElement, {
      attributes: true,
      subtree: true,
      childList: true
    })

    this.changeConfig(config)
  }
  changeConfig(config: Omit<Config, 'targetElement'>) {
    if (this.changeObserver && config.isDisplay) {
      this.config = Object.assign(this.config, config)
      this.getContent()
    }
  }
  async changeStyleDom(mutations: MutationRecord[]) {
    const targetElement = await this.getTargetElementDom()
    const {
      canvas,
      config: { isDisplay }
    } = this
    if (targetElement.children.length && targetElement.children[0] !== canvas && isDisplay) {
      this.drawCanvas()
      const referenceNode = targetElement.firstChild
      referenceNode && referenceNode.before(canvas)
    }
    if (mutations.length && mutations[0].attributeName === 'style' && mutations[0].target === canvas) {
      Object.assign(canvas.style, canvasStyle)
      Array.from(canvas.style).forEach(c => !Object.keys(canvasStyle).includes(c) && canvas.style.removeProperty(c))
    }
  }
  async getContent() {
    const {
      config: { image, text = 'xy Admin' }
    } = this
    if (image?.src) {
      const img = new Image()
      img.src = image.src
      img.crossOrigin = 'anonymous'
      img.referrerPolicy = 'no-referrer'
      img.onload = () => {
        this.content = img
        this.drawCanvas()
      }
      img.onerror = () => {
        this.content = Array.isArray(text) ? text : text.split(/,|，/g)
        this.drawCanvas()
      }
    } else {
      this.content = Array.isArray(text) ? text : text.split(/,|，/g)
      this.drawCanvas()
    }
  }
  async drawCanvas() {
    const targetElement = await this.getTargetElementDom()
    const {
      canvas,
      ctx,
      content,
      config: {
        gap: [gapX, gapY] = [100, 100],
        offset: [offsetX, offsetY] = [0, 0],
        rotate = -22,
        font: { fontSize, fontFamily, fontWeight, color, textAlign } = {},
        image
      }
    } = this

    !targetElement.contains(canvas) && targetElement.appendChild(canvas)

    const { width, height } = targetElement.getBoundingClientRect()
    canvas.setAttribute('width', `${width}px`)
    canvas.setAttribute('height', `${height}px`)
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    let [rotateW, rotateH, ownW, ownH] = [0, 0, 0, 0]
    if (content instanceof HTMLImageElement) {
      ;[ownW, ownH] = [content.width * (image?.scale ?? 1), content.height * (image?.scale ?? 1)]
      rotateW = ownW * Math.cos(Math.abs(rotate) * (Math.PI / 180))
      rotateH = ownW * Math.sin(Math.abs(rotate) * (Math.PI / 180))
    } else {
      ctx.fillStyle = color ?? '#0000004d'
      ctx.textAlign = textAlign ?? 'center'
      ctx.font = `${fontWeight ?? 'normal'} ${fontSize ?? 12}px ${fontFamily ?? 'Arial'}`
      ownW = Math.max(
        ...content.map((c: string) => {
          return ctx.measureText(c).width
        })
      )
      ownH = fontSize ?? 12
      rotateW = ownW * Math.cos(Math.abs(rotate) * (Math.PI / 180))
      rotateH = ownW * Math.sin(Math.abs(rotate) * (Math.PI / 180))
    }

    for (let i = 0; i < Math.ceil(width / (rotateW + gapX)); i++) {
      for (let j = 0; j < Math.ceil(height / (rotateH + gapY)); j++) {
        ctx.save()
        if (content instanceof HTMLImageElement) {
          ctx.translate((rotateW + gapX) * i + offsetX, (rotateH + gapY) * j + offsetY + ownH + rotateH / 2)
          ctx.rotate((rotate * Math.PI) / 180)
          ctx.drawImage(content, 0, 0, ownW, ownH)
        } else {
          ctx.translate(
            (rotateW + gapX) * i + offsetX + ownW / 2 + ownH / 3,
            (rotateH + gapY) * j + offsetY + ownH + rotateH / 2
          )
          ctx.rotate((rotate * Math.PI) / 180)
          let numY = 0
          content.forEach(c => {
            ctx.fillText(c, 0, numY)
            numY += ownH * 1.2
          })
        }
        ctx.restore()
      }
    }
  }
  async destroy() {
    this.config.isDisplay = false
    const targetElement = await this.getTargetElementDom()
    const { resizeObserver, changeObserver, canvas } = this
    if (window.ResizeObserver) {
      if (resizeObserver) {
        resizeObserver.disconnect()
        this.resizeObserver = null
      }
    } else {
      window.removeEventListener('resize', this.drawCanvas)
    }
    if (changeObserver) {
      changeObserver.disconnect()
      this.changeObserver = null
    }
    targetElement.contains(canvas) && targetElement.removeChild(canvas)
  }
}

export default Watermark
